package org.dfl.core.model.transitions;

import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.log4j.Logger;
import org.dfl.core.data.types.MapType;
import org.dfl.core.data.types.Type;
import org.dfl.core.data.types.TypeContainer;
import org.dfl.core.data.types.TypeManager;
import org.dfl.core.model.IdGenerator;
import org.dfl.core.model.TransitionExecutionException;
import org.dfl.core.model.XMLLoaderException;
import org.dfl.core.utils.Utils;
import org.dfl.messages.Messages;
import org.embl.ebi.escience.baclava.DataThing;
import org.embl.ebi.escience.baclava.factory.DataThingFactory;
import org.embl.ebi.escience.scufl.DataConstraint;
import org.embl.ebi.escience.scufl.DuplicateProcessorNameException;
import org.embl.ebi.escience.scufl.InputPort;
import org.embl.ebi.escience.scufl.OutputPort;
import org.embl.ebi.escience.scufl.Processor;
import org.embl.ebi.escience.scufl.ProcessorCreationException;
import org.embl.ebi.escience.scufl.ScuflModel;
import org.embl.ebi.escience.scufl.parser.XScuflFormatException;
import org.embl.ebi.escience.scuflworkers.ProcessorEditor;
import org.embl.ebi.escience.scuflworkers.ProcessorFactory;
import org.embl.ebi.escience.scuflworkers.ProcessorHelper;
import org.embl.ebi.escience.scuflworkers.ProcessorTaskWorker;
import org.jdom.Element;

import uk.ac.soton.itinnovation.taverna.enactor.entities.TaskExecutionException;

/**
 * transition that creates a taverna processor
 * @author Piotr Wlodarczyk
 *
 */
public class TavernaTransition extends FixedInputsTransition {
	Logger logger = Logger.getLogger(TavernaTransition.class);
	
	private Processor processor;
	private ProcessorEditor processorEditor = null;
	private Element processorElem;
	private boolean outputsLoaded = false;
	private boolean inputsLoaded = false;
	ProcessorFactory factory = null;
	
	public TavernaTransition(){
		super();
	}
	
	public TavernaTransition(ProcessorFactory factory){
		super();
		this.factory = factory;
	}
	
	public void initProcessor()  throws ProcessorCreationException, DuplicateProcessorNameException{
		setName(Utils.stripTags(factory.getName()));
		Processor p = factory.createProcessor(this.getId(),new ScuflModel());
		setDescription(Utils.stripTags(p.getDescription()));		
		setProcessor(p);
	}
	
	public TavernaTransition(Processor processor) {
		setProcessor(processor);
	}

	public void setProcessor(Processor processor) {
		if(processor == null) return;
		if(this.id == null && (this.processor==null || processor==null || (this.processor.getClass() != processor.getClass()))){
			setId(null);
		}
		this.processor = processor;
		
		String tagName = ProcessorHelper.getTagNameForClassName(processor.getClass().getName());
		processorEditor = ProcessorHelper.getEditorForTagName(tagName);
		
		// auto (re)init
		initInputDefinitions();
	}
	
	@Override
	protected void regenerateId() {
		if(getName()!=null && !getName().equals("")){ //$NON-NLS-1$
			String possibleId = Utils.stripTags(getName()).replaceAll("[^a-z^A-Z]", ""); //$NON-NLS-1$ //$NON-NLS-2$
			id = IdGenerator.getGeneratedId(possibleId);
		}else{
			super.regenerateId();
		}
	}

//	@Override
//	protected void init() {
//		if(this.processor == null) return;
//		super.init();
//	}
	
	@Override
	protected Type getDefaultOutputDefinition() {
		return TypeManager.getAnyType();
	}
	
	@Override
	protected void initInputDefinitions() {
		if(this.processor == null) return;
		// if processor instance exist and inputs where not created by load() method then init them
		if(!inputsLoaded){
			//set inputs
			InputPort[] inputs = getProcessor().getInputPorts();
			getInputsDefinition().clear();
			if(inputs != null)
			for(int i=0; i<inputs.length; i++){
				try {
					addInputDefinition(inputs[i].getName(),TypeManager.getTavernaType(inputs[i].getSyntacticType()));
				} catch (Exception e) {
					// should not happened!
				}
			}
		}
		if(!outputsLoaded){
			//set outputs 
			OutputPort[] outputs = getProcessor().getOutputPorts();
			if(outputs!= null){
				if(outputs.length == 1){
					// jeden wynik w procesorze = prosty typ wynikowy 
					setOutputType(TypeManager.getTavernaType(outputs[0].getSyntacticType()));	
				}else{
					MapType m = (MapType) TypeManager.getMapType();
					// wiele zlozony wynik procesora - typ mapy na wyjsciu z tranzycji
					for(int i=0; i<outputs.length; i++){ 
						m.addSubkey(outputs[i].getName(),TypeManager.getTavernaType(outputs[i].getSyntacticType()));
					}
					m.setFixed(true);
					setOutputType(m);
				}
			}
		}
		calculateMinInputs();
	}
	

	public void calculateMinInputs() {
		// if has many inputs then at least one has to be defined
		if(inputsDefinition.size()>0){
			minInputs = 1;
		}		
	}

	@SuppressWarnings("unchecked") //$NON-NLS-1$
	@Override
	protected Object fire(Map<String, Object> inputMap) throws TransitionExecutionException {
		ProcessorTaskWorker processorTaskWorker;
		try {
			processorTaskWorker = ProcessorHelper.getTaskWorker(getProcessor());
		} catch (Exception e1) {
			throw new TransitionExecutionException(e1);
		}
		
		try {
			Map<String, DataThing> inputMapDT = new HashMap<String, DataThing>();
			Iterator iter = inputMap.keySet().iterator();
			while(iter.hasNext()){
				String key = (String) iter.next();
				inputMapDT.put(key, DataThingFactory.bake(inputMap.get(key)));
			}
			
			// poprawienie w modelu
			ScuflModel model = getProcessor().getModel();
			DataConstraint dc[] = model.getDataConstraints();
			for (int i = 0; i < dc.length; i++) {
				model.destroyDataConstraint(dc[i]);
			}
			int i =0;
			InputPort[] inputs = getProcessor().getInputPorts();
			for(InputPort input : inputs){
				try {
					if(inputMap.containsKey(input.getName())){
						model.addDataConstraint(new DataConstraint(model,new OutputPort(getProcessor(),getProcessor().getName()+"i"+i++),input)); //$NON-NLS-1$
					}
				} catch (Exception e) {
					logger.error(e.getMessage(), e);
				}
			}
			i=0;
			OutputPort[] outputs = getProcessor().getOutputPorts();
			for(OutputPort output : outputs){
				try {
					model.addDataConstraint(new DataConstraint(model,output,new InputPort(getProcessor(),getProcessor().getName()+"o"+i++))); //$NON-NLS-1$
				} catch (Exception e) {
					logger.error(e.getMessage(), e);
				}
			}
			
			// odpal procesor
//			System.out.println("processorTaskWorker.execute start");
			Map<String,DataThing> outputMapDT = processorTaskWorker.execute(inputMapDT, null);
//			System.out.println("processorTaskWorker.execute stop");
			// przepisz wyniki 
			if(outputMapDT.keySet().size()==1){
				iter = outputMapDT.keySet().iterator();
				String key = (String) iter.next();
				Object ob = outputMapDT.get(key).getDataObject();
				return translateData(ob,getOutputPortMimeTypeList(outputs,key));
			}else{
				Map<String, Object> outputMap = new HashMap<String, Object>();
				iter = outputMapDT.keySet().iterator();
				while(iter.hasNext()){
					String key = (String) iter.next();
					Object ob = outputMapDT.get(key).getDataObject();
					outputMap.put(key, translateData(ob,getOutputPortMimeTypeList(outputs,key)));
				}
				return outputMap;
			}
		} catch (TaskExecutionException e) {
			throw new TransitionExecutionException(e);
		}
	}
	
	@SuppressWarnings("unchecked") //$NON-NLS-1$
	private Object translateData(Object ob, List mimeTypesList ) {
//		enable this to translate keep binary data with its mime type - currently not used!
//		if(ob instanceof Collection){
//			Collection res = new ArrayList();
//			for(Object o : (Collection) ob){
//				res.add(translateData(o, mimeTypesList));
//			}
//			return res;
//		}else if(ob instanceof byte[]){
//			return new BinaryData(mimeTypesList,(byte[]) ob);
//		}
		return ob;
	}

	@SuppressWarnings("unchecked") //$NON-NLS-1$
	private List getOutputPortMimeTypeList(OutputPort[] outputs, String key) {
		for(OutputPort output : outputs){
			if(output.getName().equals(key)){
				return output.getMetadata().getMIMETypeList();
			}
		}
		return null;
	}

	@SuppressWarnings("unchecked") //$NON-NLS-1$
	@Override
	public void load(Element elem) throws XMLLoaderException {
		super.load(elem);
		try {
			processorElem = elem.getChild("toolspecific").getChild("processor",Transition.toolspecificNS); //$NON-NLS-1$ //$NON-NLS-2$
//			setProcessor(loadProcessor(processorElem,getId()));
		}catch (Exception e) {		}
		
		if(processorElem==null){
			throw new XMLLoaderException(Messages.TavernaTransition_errorLoadFaild);
		}
		
		try{
			if(processor==null){
				List<Element> ins = elem.getChild("toolspecific").getChildren("input"); //$NON-NLS-1$ //$NON-NLS-2$
				if (ins != null && ins.size()>0) {
					for (Element in : ins) {
						addInputDefinition(in.getAttributeValue("key"), TypeManager.load(in.getTextTrim())); //$NON-NLS-1$
					}
					inputsLoaded=true;
				}
				Element out = elem.getChild("toolspecific").getChild("output"); //$NON-NLS-1$ //$NON-NLS-2$
				if (out != null) {
					setOutputType(TypeManager.load(out.getTextTrim()));
					outputsLoaded = true;
				}
				
				calculateMinInputs();
			}
		} catch (Exception e) {
			throw new XMLLoaderException(Messages.TavernaTransition_errorLoadFaild + getId(),e);
		}
	}
	
	@Override
	public Element save() {
		Element elem = super.save();
		
		Element toolspecificElem = elem.getChild("toolspecific"); //$NON-NLS-1$
		if(toolspecificElem == null){
			toolspecificElem = new Element("toolspecific"); //$NON-NLS-1$
			toolspecificElem.setAttribute("tool",""); //$NON-NLS-1$ //$NON-NLS-2$
			toolspecificElem.setAttribute("version",""); //$NON-NLS-1$ //$NON-NLS-2$
			elem.addContent(toolspecificElem);
		}
		
		Element outputElem = new Element("output"); //$NON-NLS-1$
		outputElem.addContent(getOutputTypeContainer().getType().toString());
		toolspecificElem.addContent(outputElem);
		
		Map<String, TypeContainer> inputs = getInputsDefinition();
		Set<String> inputsKeys = inputs.keySet();
		for(String key : inputsKeys){
			Element inElem = new Element("input"); //$NON-NLS-1$
			inElem.addContent(inputs.get(key).getType().toString());
			inElem.setAttribute("key", key); //$NON-NLS-1$
			toolspecificElem.addContent(inElem);
		}
		
		Element processorElem = new Element("processor", Transition.toolspecificNS); //$NON-NLS-1$
		Processor p = getProcessor();
		if(p!=null){
			processorElem.addContent(ProcessorHelper.elementForProcessor(p));
			toolspecificElem.addContent(processorElem);
		}else if(this.processorElem!=null){
			toolspecificElem.addContent((Element)this.processorElem.clone());
		}else{
			// should not happen
			throw new RuntimeException(Messages.TavernaTransition_9);
		}
		
		return elem;
	}
	
	private Processor loadProcessor(Element processorNode, String name) throws Exception{
		// load processor
		Processor theProcessor = null;
		try{
			theProcessor = org.embl.ebi.escience.scuflworkers.ProcessorHelper
					.loadProcessorFromXML(processorNode, new ScuflModel(), name);
		}catch(Throwable e){e.printStackTrace();}
		
		// if something wrong
		if (theProcessor == null) {
			throw new XScuflFormatException(
					Messages.TavernaTransition_errorNoSpecificationMechanizm1	+ Messages.TavernaTransition_errorNoSpecificationMechanizm2 + name + Messages.TavernaTransition_errorNoSpecificationMechanizm3);
		}
		
		String logLevel = processorNode.getAttributeValue("log"); //$NON-NLS-1$
		// Default value for a processor is -1, which translates to
		// 'inherit from model'.
		int log = -1;
		if (logLevel != null) {
			log = Integer.parseInt(logLevel);
		}
		theProcessor.setLogLevel(log);
		return theProcessor;
	}
	
	@Override
	public String getDefaultDescription() {
		if(getProcessor() != null && getProcessor().getDescription()!=null){
			return getProcessor().getDescription();
		}else{
			return Messages.TavernaTransition_errorEmptyProcessor;
		}
	}

	public Processor getProcessor() {
		if(processor==null && processorElem!=null){
			try {
				setProcessor(loadProcessor(processorElem,getId()));
			} catch (Exception e) {
				new RuntimeException(e);
			}
		}
		return processor;
	}

	public ProcessorEditor getProcessorEditor() {
		if(getProcessor()==null) return null;
		return processorEditor;
	}
	
	@Override
	public void addInputDefinition(String key, Type type) throws Exception {
		super.addInputDefinition(key, type);
		addOptionalInput(key);
	}
	
	@Override
	public void removeInputDefinition(String key) {
		super.removeInputDefinition(key);
		removeOptionalInput(key);
	}
}